#include "lua_bind.hh"
#include <sstream>
#include <vector>
#include <unordered_map>
#include <tuple>
#include "../../protocol_files/common/generated_proto_header.hh"
#include "../../protocol_files/lua_rpc/rpc_generated.hh"
#include "../../corelib/util/string_util.hh"
#include "../../plugin/base/entity.hh"
#include "../../plugin/base/rpc_base.hh"
#include "lua_helper.hh"

bool LuaBinder::callLuaImplMethod(const char* interfaceName, const char* methodName,
    PathID pathID, std::uint64_t uuid, std::uint64_t rpcID, const google::protobuf::Message& req,
    google::protobuf::Message & ack) {
    auto coi = getCoManager().getFree();
    lua_State* L = *coi;
    StackBalancer sb(L);
    lua_getglobal(L, interfaceName);
    lua_pushstring(L, methodName);
    lua_gettable(L, -2);
    toLua(*coi, req);
    auto status = lua_resume(L, nullptr, 1);
    if (status == LUA_OK) {
        if (lua_istable(L, -1)) {
            copyTableLuaToC(L, ack);
            getCoManager().addFree(coi);
        }
        return true;
    } else if (status == LUA_YIELD) {
        getCallManager().add(coi, &ack, pathID, uuid, rpcID, 0);
        getCoManager().addBusy(coi);
        return false;
    } else {
        catchError(L, status);
        RPCRef.deletePBProtoMessage(ack);
        getCoManager().remove(coi);
        return false;
    }
}

LuaBinder::LuaBinder()
    : virtualMachine_(nullptr), start_(false) {
}

LuaBinder::~LuaBinder() {
}

lua_State * LuaBinder::getVirtualMachine() {
    return virtualMachine_;
}

void timerCallback(TimerHandle id, UserData) {
    auto* L = LuaRef.getTimer().removeTimer(id);
    if (!L) {
        errorlog << "Timer owner not found" << endlog;
        return;
    }
    // Resume yield thread
    catchError(L, lua_resume(L, nullptr, 0));
}

int LuaBinder::sleep(lua_State * L) {
    if (!lua_isinteger(L, -1)) {
        raiseError(L, "sleep() need a integer parameter");
        return 0;
    }
    auto timeout = (time_t)lua_tointeger(L, -1);
    // Starts a new timer
    auto timerID = FrameworkRef.startTimerOnce(timeout, timerCallback, 0);
    LuaRef.getTimer().addTimer(timerID, L);
    // Yield
    return lua_yieldk(L, 0, 0, nullptr);
}

int LuaBinder::uuid(lua_State * L) {
    auto uuid = ModuleRef.getUUID();
    lua_pushinteger(L, uuid);
    return 1;
}

int LuaBinder::findPath(lua_State* L) {
    int nargs = lua_gettop(L);
    if (!nargs) {
        raiseError(L, "Invalid parameter");
        return 0;
    }
    if (!lua_isinteger(L, -1)) {
        raiseError(L, "SiteType for 'findPath' is should be integer");
        return 0;
    }
    auto siteType = (SiteType)lua_tointeger(L, -1);
    auto findSerial = LuaRef.getPathFinderManager().add(siteType, L);
    auto ret = FrameworkRef.findPath(siteType, findSerial);
    if (!ret) {
        // Cannot raise finding
        LuaRef.getPathFinderManager().remove(findSerial);
        lua_pushnil(L);
        return 1;
    }
    return lua_yieldk(L, 0, 0, nullptr);
}

int LuaBinder::ret(lua_State* L) {
    // return to RPC
    auto call = LuaRef.getCallManager().get(L);
    if (!call) {
        return 1;
    }
    auto top = lua_gettop(L);
    if (top != 1) {
        errorlog << "Invalid parameter for rpcret" << endlog;
        lua_pop(L, top);
        return 0;
    }
    auto coi = call->coi;
    copyTableLuaToC(L, *call->ack);
    FrameworkRef.cancelTimer(call->expiredTimerID);
    // send ack to path
    if (call->fromPathID) {
        sendEntityAck(call->pathID, call->fromPathID, call->uuid, call->rpcID, *call->ack);
    } else {
        sendPathAck(call->pathID, call->uuid, call->rpcID, *call->ack);
    }
    // delete RPC call
    LuaRef.getCallManager().remove(L);
    LuaRef.getCoManager().addFree(coi);
    lua_pop(L, 1);
    return 0;
}

int LuaBinder::abort(lua_State* L) {
    if (lua_isstring(L, -1)) {
        lua_pushstring(L, lua_tostring(L, -1));
    } else {
        lua_pushliteral(L, "abort");
    }
    lua_error(L);
    return 0;
}

int LuaBinder::closePath(lua_State* L) {
    if (!lua_isinteger(L, -1)) {
        lua_pop(L, 1);
        return 0;
    }
    auto pathID = (PathID)lua_tointeger(L, -1);
    FrameworkRef.closePath(pathID);
    lua_pop(L, 1);
    return 0;
}

int LuaBinder::findEntity(lua_State * L) {
    int nargs = lua_gettop(L);
    if (nargs == 0) {
        errorlog << getStackInfo(L) << "Need a parameter" << endlog;
        lua_pushnil(L);
        return 1;
    }
    if (!lua_isinteger(L, 1)) {
        errorlog << getStackInfo(L) << "EntityType for 'findEntity' is should be integer" << endlog;
        return 0;
    }
    std::time_t timeout = 2000;
    if (nargs == 2) {
        timeout = (std::time_t)lua_tointeger(L, 2);
    }
    auto entityType = (EntityType)lua_tointeger(L, 1);
    lua_pop(L, nargs);
    auto findSerial = LuaRef.getEntityFinderManager().add(entityType, L);
    auto ret = FrameworkRef.searchEntity(entityType, nullptr, SearchType::ROUNDROBIN, timeout, findSerial);
    if (!ret) {
        LuaRef.getPathFinderManager().remove(findSerial);
        lua_pushnil(L);
        return 1;
    }
    if (LuaRef.getEntityFinderManager().get(findSerial)) {
        // Waiting for result
        return lua_yieldk(L, 0, 0, nullptr);
    } else {
        // Return immediately
        auto entityID = LuaRef.getEntityFinderManager().getImmeEntityID();
        lua_pushinteger(L, entityID);
        return 1;
    }
}

int LuaBinder::create(lua_State* L) {
    auto nargs = lua_gettop(L);
    if (!lua_isfunction(L, 1)) {
        raiseError(L, "Need a lua function");
        return 0;
    }
    auto coi = CoInfo::newThread();
    lua_xmove(L, *coi, nargs);
    LuaRef.getUserCoManager().add(coi);
    lua_pushinteger(L, coi->ref());
    return 1;
}

int LuaBinder::spawn(lua_State * L) {
    auto nargs = lua_gettop(L);
    if (!lua_isfunction(L, 1)) {
        raiseError(L, "Need a lua function");
        return 0;
    }
    auto coi = CoInfo::newThread();
    lua_xmove(L, *coi, nargs);
    LuaRef.getUserCoManager().add(coi);
    coi->setResumer(L);
    catchError(*coi, lua_resume(*coi, nullptr, nargs - 1));
    lua_pushinteger(L, coi->ref());
    return 1;
}

int LuaBinder::resume(lua_State* L) {
    if (!lua_isinteger(L, 1)) {
        raiseError(L, "Resume need a user coroutine");
        return 0;
    }
    auto ref = (int)lua_tointeger(L, 1);
    auto coi = LuaRef.getUserCoManager().getCoByRef(ref, L);
    if (!coi) {
        raiseError(L, "Coroutine not found");
        return 0;
    }
    auto nargs = lua_gettop(L);
    lua_xmove(L, *coi, nargs - 1);
    coi->setResumer(L);
    lua_pop(L, 1);
    catchError(*coi, lua_resume(*coi, nullptr, nargs - 1));
    auto status = lua_status(*coi);
    if (LUA_OK == status) {
        auto nret = lua_gettop(*coi);
        lua_xmove(*coi, L, nret);
        return nret;
    } else if (LUA_YIELD == status) {
        auto nret = lua_gettop(L);
        return nret;
    }
    return 0;
}

int LuaBinder::yield(lua_State* L) {
    auto nargs = lua_gettop(L);
    auto resumer = LuaRef.getUserCoManager().getResumer(L);
    if (!resumer) {
        raiseError(L, "Resumer not found");
        return 0;
    }
    auto coi = LuaRef.getUserCoManager().getCoByL(resumer);
    if (!coi) {
        raiseError(L, "Resumer not found");
        return 0;
    }
    coi->setResumer(nullptr);
    lua_xmove(L, *coi, nargs);
    return lua_yield(L, nargs);
}

int LuaBinder::close(lua_State * L) {
    auto nargs = lua_gettop(L);
    for (int i = 1; i <= nargs; i++) {
        if (!lua_isinteger(L, i)) {
            raiseError(L, "Resume need a user coroutine");
            return 0;
        }
        auto ref = (int)lua_tointeger(L, i);
        auto coi = LuaRef.getUserCoManager().getCoByRef(ref, L);
        if (!coi) {
            raiseError(L, "Coroutine not found");
            return 0;
        }
        LuaRef.getUserCoManager().remove(coi);
        lua_settop(*coi, 0);
        CoInfo::deleteThread(coi);
    }    
    return 0;
}
